﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using HtmlAgilityPack;

namespace AWLoadTesterLibrary
{
    /// <summary>
    /// Represents the result of a single user's browse session
    /// </summary>
    public class LoadTestUserResult
    {
        /// <summary>
        /// The User this result belongs too
        /// </summary>
        public LoadTestUser User { get; set; }

        /// <summary>
        /// The list of HTTP Status codes generated from this user's test run
        /// </summary>
        public List<int> HTTPCodes { get; set; }

        /// <summary>
        /// Notes if the user's test run is completed or not
        /// </summary>
        public bool Completed
        {
            get
            {
                return StopTime > DateTime.MinValue;
            }
        }

        /// <summary>
        /// Notes if the run was successful or not.  
        /// </summary>
        public bool Successful { get; set; }

        /// <summary>
        /// The test start time for this session.
        /// </summary>
        public DateTime StartTime { get; set; }

        /// <summary>
        /// The test end time for this session
        /// </summary>
        public DateTime StopTime { get; set; }

        /// <summary>
        /// Creates a new user test result object.
        /// </summary>
        public LoadTestUserResult()
        {
            HTTPCodes = new List<int>();
            User = null;

            StartTime = StopTime = DateTime.MinValue;

            Successful = false;
        }
    }

    /// <summary>
    /// This class represents a load test user.
    /// <para>
    /// These users access the web on separate threads and interact with a website for a limited time.
    /// </para>
    /// </summary>
    ///
    public class LoadTestUser
    {
        /// <summary>
        /// A general purpose utility option that's unique to each user.
        /// </summary>
        public LoadTesterUtilsClass Util { get; set; }

        /// <summary>
        /// This Method connect to a URL and browses as a user.
        /// </summary>
        /// <param name="opts">The Options object containing URL, and other data for the browse session.</param>
        /// <param name="conns">The number of simultaneous connections the "browser" is allowed to make.  As of now this is approximated.</param>
        /// <param name="browseTime">The user will "pause" a random number of seconds between "clicks" i.e. Pause before following links like a normal user.</param>
        /// <returns>An "LoadTestUserResult" object that contains all the HTTP Code and other test status information for this run.</returns>
        public LoadTestUserResult Browse(LoadTestRunOptions opts, int conns, int browseTime)
        {
            LoadTestUserResult res = new LoadTestUserResult();

            res.StartTime = DateTime.UtcNow;
            res.User = this;

            try
            {
                Util.CurrentUrl = opts.URL;
                Util.RandomSleepMax = browseTime * 1000;
                Util.SleepAfterEveryRequest = browseTime * 1000;

                // Manage the active threads.  Make sure they don't go over the connection limit
                List<Thread> activeCons = new List<Thread>();

                HtmlDocument currDoc = Util.GetResponseDocument(new Uri(opts.URL), 5);
                if (currDoc.DocumentNode != null)
                {
                    // We can go the easy route and just download everything with a "src" or "href" attribute.
                    // But I suspect we'll need the granular handling in the future and it's not that much work.


                    List<LoadTesterLink> linksTemp = new List<LoadTesterLink>();
                    HtmlNodeCollection NodesTemp;

                    IEnumerable<LoadTesterLink> imgList = null;
                    IEnumerable<LoadTesterLink> linkList = null;
                    IEnumerable<LoadTesterLink> scriptList = null;
                    IEnumerable<LoadTesterLink> spanList = null;
                    IEnumerable<string> styleList = null;

                    // Get the images
                    NodesTemp = currDoc.DocumentNode.SelectNodes("//img[@src]");
                    if (NodesTemp != null)
                    {
                        imgList = NodesTemp.Where(a => a.Attributes["src"] != null && a.Attributes["src"].Value != null)
                                         .Select(a => new LoadTesterLink { LinkString = a.Attributes["src"].Value, LinkType = LoadTesterLinkType.IMG });
                        linksTemp.AddRange(imgList);
                    }

                    // Get the links
                    NodesTemp = currDoc.DocumentNode.SelectNodes("//link[@href]");
                    if (NodesTemp != null)
                    {
                        linkList = NodesTemp.Where(a => a.Attributes["href"] != null && a.Attributes["href"].Value != null)
                                        .Select(a => new LoadTesterLink { LinkString = a.Attributes["href"].Value, LinkType = LoadTesterLinkType.LINK_HREF });
                        linksTemp.AddRange(linkList);
                    }

                    // Get the scripts
                    NodesTemp = currDoc.DocumentNode.SelectNodes("//script[@src]");
                    if (NodesTemp != null)
                    {
                        scriptList = NodesTemp.Where(a => a.Attributes["src"] != null && a.Attributes["src"].Value != null)
                                        .Select(a => new LoadTesterLink { LinkString = a.Attributes["src"].Value, LinkType = LoadTesterLinkType.SCRIPT });
                        linksTemp.AddRange(scriptList);
                    }

                    // Get the spans
                    NodesTemp = currDoc.DocumentNode.SelectNodes("//span[@src]");
                    if (NodesTemp != null)
                    {
                        spanList = NodesTemp.Where(a => a.Attributes["src"] != null && a.Attributes["src"].Value != null)
                                        .Select(a => new LoadTesterLink { LinkString = a.Attributes["src"].Value, LinkType = LoadTesterLinkType.None });
                        linksTemp.AddRange(spanList);
                    }

                    // Grab all easy body CSS Links
                    NodesTemp   = currDoc.DocumentNode.SelectNodes("//*[@style]");
                    if (NodesTemp != null)
                    {
                        styleList = NodesTemp.Where(a => a.Attributes["style"] != null && a.Attributes["style"].Value != null)
                                        .Select(a => a.Attributes["style"].Value);
                        foreach (string cssStr in styleList)
                        {
                            linksTemp.AddRange(Util.GetLinksFromCSS(cssStr));
                        }
                    }
                    // Grab all easy body Javascript links
                    // FIXME: TODO

                    // Now process all the download links we have
                    
                    Queue<LoadTesterLink> links = new Queue<LoadTesterLink>();
                    foreach (LoadTesterLink currLink in linksTemp)
                    {
                        if (String.IsNullOrWhiteSpace(currLink.LinkString))
                        {
                            continue;
                        }

                        currLink.UpdateUri(Util.LastResponse.ResponseUri);

                        links.Enqueue(currLink);
                    }

                    // Retrieve links
                    while (links.Count>0)
                    {
                        List<Thread> currBrowseThreads = new List<Thread>();
                        for (int i = 0; i < conns; i++)
                        {
                            if (links.Count < 1)
                            {
                                break;
                            }

                            LoadTesterLink currLink = links.Dequeue();

                            
                            Thread userBrowseThread = new Thread(new ThreadStart(() =>
                            {
                                List<LoadTesterLink> CSSLinks = null;
                                HtmlDocument currTempDoc = Util.GetResponseDocument(currLink.LinkUri, 5);
                                if (currLink.LinkType == LoadTesterLinkType.LINK_HREF && currTempDoc != null)
                                {
                                    using (StringWriter sw = new StringWriter())
                                    {
                                        currTempDoc.Save(sw);
                                        CSSLinks = Util.GetLinksFromCSS(sw.ToString());
                                    }
                                    
                                    // Get and parse CSS
                                    // FIXME: This does not honor the connection limit
                                    foreach (LoadTesterLink cssLink in CSSLinks)
                                    {
                                        cssLink.UpdateUri(Util.LastResponse.ResponseUri);
                                        currTempDoc = Util.GetResponseDocument(cssLink.LinkUri, 5);
                                    }
                                }

                                Thread.CurrentThread.Abort();
                            }));
                            userBrowseThread.Name = "AWLoadTesterTestThread";
                            userBrowseThread.IsBackground = true;
                            userBrowseThread.Start();

                            opts.ThreadList.Add(userBrowseThread);
                            currBrowseThreads.Add(userBrowseThread);
                        }

                        // Wait for the threads. But not too long
                        // FIXME: This should be event driven
                        int maxWait = 0;
                        while (currBrowseThreads.Count(t => t.IsAlive) > 0 && maxWait < 100)
                        {
                            Thread.Sleep(100);
                        }

                    }
                    
                    Util.RandomSleep();
                }
            }
            catch (Exception ex)
            {
                res.Successful = false;
                return res;
            }
            finally
            {
                res.StopTime = DateTime.UtcNow;
            }

            res.Successful = true;
            return res;
        }

        /// <summary>
        /// Creates a new load test user.
        /// </summary>
        public LoadTestUser()
        {
            Util = new LoadTesterUtilsClass();
        }
    }

}
